from .types import *
from pathlib import Path
from typing import TextIO
import os
import re

def camel2snake(str):
    return re.sub(r'(?<!^)(?=[A-Z])', '_', str).upper()

def _write_metric(f: TextIO, metric: Metric, prefix: str):
    full_name = camel2snake(metric.name)
    description = ' '.join([line.strip() for line in metric.description.split('\n')]).strip()
    converter = 'NONE'
    if isinstance(metric, HistogramMetric):
        converter = metric.converter.name
    elif hasattr(metric, 'converter'):
        converter = metric.converter.name

    f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_OFF  ({metric.offset}UL)\n')
    f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_NAME "{prefix}_{full_name.lower()}"\n')
    f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_TYPE (FD_METRICS_TYPE_{metric.type.name})\n')
    f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_DESC "{description}"\n')
    f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_CVT  (FD_METRICS_CONVERTER_{converter})\n')

    if isinstance(metric, GaugeEnumMetric) or isinstance(metric, CounterEnumMetric):
        f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_CNT  ({len(metric.enum.values)}UL)\n\n')
        for idx, value in enumerate(metric.enum.values):
            f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_{camel2snake(value.name)}_OFF ({metric.offset+idx}UL)\n')

    if isinstance(metric, HistogramMetric):
        if metric.converter == HistogramConverter.SECONDS:
            min_str = str(float(metric.min))
            max_str = str(float(metric.max))
        else:
            min_str = str(int(metric.min)) + "UL"
            max_str = str(int(metric.max)) + "UL"

        f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_MIN  ({min_str})\n')
        f.write(f'#define FD_METRICS_{metric.type.name.upper()}_{prefix.upper()}_{full_name}_MAX  ({max_str})\n')

    f.write('\n')

def _write_metric_descriptor(f, full_name, metric: Metric):
    if isinstance(metric, CounterMetric):
        f.write(f'    DECLARE_METRIC( {full_name}, COUNTER ),\n')
    elif isinstance(metric, GaugeMetric):
        f.write(f'    DECLARE_METRIC( {full_name}, GAUGE ),\n')
    elif isinstance(metric, CounterEnumMetric):
        for value in metric.enum.values:
            f.write(f'    DECLARE_METRIC_ENUM( {full_name}, COUNTER, {camel2snake(metric.enum.name)}, {camel2snake(value.name)} ),\n')
    elif isinstance(metric, GaugeEnumMetric):
        for value in metric.enum.values:
            f.write(f'    DECLARE_METRIC_ENUM( {full_name}, GAUGE, {camel2snake(metric.enum.name)}, {camel2snake(value.name)} ),\n')
    elif isinstance(metric, HistogramMetric):
        if metric.converter == HistogramConverter.SECONDS:
            f.write(f'    DECLARE_METRIC_HISTOGRAM_SECONDS( {full_name} ),\n')
        elif metric.converter == HistogramConverter.NONE:
            f.write(f'    DECLARE_METRIC_HISTOGRAM_NONE( {full_name} ),\n')
        else:
            raise Exception(f'Unknown histogram converter: {metric.converter}')
    else:
        raise ValueError("Unknown metric type")
    pass

def _write_common(metrics: Metrics):
    with open(Path(__file__).parent / '../generated' / 'fd_metrics_all.h', 'w') as f:
        f.write('#ifndef HEADER_fd_src_disco_metrics_generated_fd_metrics_all_h\n')
        f.write('#define HEADER_fd_src_disco_metrics_generated_fd_metrics_all_h\n\n')
        f.write('/* THIS FILE IS GENERATED BY gen_metrics.py. DO NOT HAND EDIT. */\n\n')
        f.write('#include "../fd_metrics_base.h"\n\n')
        for tile in metrics.tiles.keys():
            f.write(f'#include "fd_metrics_{tile.name.lower()}.h"\n')

        f.write('/* Start of LINK OUT metrics */\n\n')
        for metric in metrics.link_out:
            _write_metric(f, metric, "link")

        f.write('/* Start of LINK IN metrics */\n\n')
        for metric in metrics.link_in:
            _write_metric(f, metric, "link")

        f.write('/* Start of TILE metrics */\n\n')
        for metric in metrics.common:
            _write_metric(f, metric, "tile")

        offset = sum([int(metric.footprint()/8) for metric in metrics.common])
        f.write(f'\n#define FD_METRICS_ALL_TOTAL ({offset}UL)\n')
        f.write(f'extern const fd_metrics_meta_t FD_METRICS_ALL[FD_METRICS_ALL_TOTAL];\n')
        f.write(f'\n#define FD_METRICS_ALL_LINK_IN_TOTAL ({len(metrics.link_in)}UL)\n')
        f.write(f'extern const fd_metrics_meta_t FD_METRICS_ALL_LINK_IN[FD_METRICS_ALL_LINK_IN_TOTAL];\n')
        f.write(f'\n#define FD_METRICS_ALL_LINK_OUT_TOTAL ({len(metrics.link_out)}UL)\n')
        f.write(f'extern const fd_metrics_meta_t FD_METRICS_ALL_LINK_OUT[FD_METRICS_ALL_LINK_OUT_TOTAL];\n')

        # Max size of any particular tiles metrics
        max_offset = 0
        for (tile, tile_metrics) in metrics.tiles.items():
            tile_offset = sum([int(metric.footprint() / 8) for metric in tile_metrics])
            if tile_offset > max_offset:
                max_offset = tile_offset

        # Kind of a hack for now.  Different tiles should get a different size.
        f.write(f'\n#define FD_METRICS_TOTAL_SZ (8UL*{max_offset+offset}UL)\n')
        f.write(f'\n#define FD_METRICS_TILE_KIND_CNT {len(metrics.tiles)}\n')
        f.write(f'extern const char * FD_METRICS_TILE_KIND_NAMES[FD_METRICS_TILE_KIND_CNT];\n')
        f.write(f'extern const ulong FD_METRICS_TILE_KIND_SIZES[FD_METRICS_TILE_KIND_CNT];\n')
        f.write(f'extern const fd_metrics_meta_t * FD_METRICS_TILE_KIND_METRICS[FD_METRICS_TILE_KIND_CNT];\n')
        f.write('\n#endif /* HEADER_fd_src_disco_metrics_generated_fd_metrics_all_h */\n')

    with open(Path(__file__).parent / '../generated' / 'fd_metrics_all.c', 'w') as f:
        f.write('/* THIS FILE IS GENERATED BY gen_metrics.py. DO NOT HAND EDIT. */\n')
        f.write('#include "fd_metrics_all.h"\n\n')

        f.write('const fd_metrics_meta_t FD_METRICS_ALL[FD_METRICS_ALL_TOTAL] = {\n')
        for metric in metrics.common:
            full_name = f'TILE_{camel2snake(metric.name)}'
            _write_metric_descriptor(f, full_name, metric)
        f.write('};\n\n')

        f.write('const fd_metrics_meta_t FD_METRICS_ALL_LINK_IN[FD_METRICS_ALL_LINK_IN_TOTAL] = {\n')
        for metric in metrics.link_in:
            full_name = f'LINK_{camel2snake(metric.name)}'
            _write_metric_descriptor(f, full_name, metric)
        f.write('};\n\n')

        f.write(f'const fd_metrics_meta_t FD_METRICS_ALL_LINK_OUT[FD_METRICS_ALL_LINK_OUT_TOTAL] = {{\n')
        for metric in metrics.link_out:
            full_name = f'LINK_{camel2snake(metric.name)}'
            _write_metric_descriptor(f, full_name, metric)
        f.write('};\n\n')

        f.write(f'const char * FD_METRICS_TILE_KIND_NAMES[FD_METRICS_TILE_KIND_CNT] = {{\n')
        for tile in Tile:
            if tile in metrics.tiles:
                f.write(f'    "{tile.name.lower()}",\n')
        f.write('};\n\n')

        f.write(f'const ulong FD_METRICS_TILE_KIND_SIZES[FD_METRICS_TILE_KIND_CNT] = {{\n')
        for tile in Tile:
            if tile in metrics.tiles:
                f.write(f'    FD_METRICS_{tile.name}_TOTAL,\n')
        f.write('};\n')

        f.write(f'const fd_metrics_meta_t * FD_METRICS_TILE_KIND_METRICS[FD_METRICS_TILE_KIND_CNT] = {{\n')
        for tile in Tile:
            if tile in metrics.tiles:
                f.write(f'    FD_METRICS_{tile.name},\n')
        f.write('};\n')


def _write_tile(tile: Tile, metrics: List[Metric]):
    with open(Path(__file__).parent / '../generated' / f'fd_metrics_{tile.name.lower()}.h', 'w') as f:
        f.write('#ifndef HEADER_fd_src_disco_metrics_generated_fd_metrics_' + tile.name.lower() + '_h\n')
        f.write('#define HEADER_fd_src_disco_metrics_generated_fd_metrics_' + tile.name.lower() + '_h\n\n')
        f.write('/* THIS FILE IS GENERATED BY gen_metrics.py. DO NOT HAND EDIT. */\n\n')
        f.write('#include "../fd_metrics_base.h"\n')
        f.write('#include "fd_metrics_enums.h"\n\n')

        for metric in metrics:
            _write_metric(f, metric, tile.name.lower())

        total = sum([int(metric.count()) for metric in metrics])
        f.write(f'#define FD_METRICS_{tile.name}_TOTAL ({total}UL)\n')
        f.write(f'extern const fd_metrics_meta_t FD_METRICS_{tile.name}[FD_METRICS_{tile.name}_TOTAL];\n')
        f.write('\n#endif /* HEADER_fd_src_disco_metrics_generated_fd_metrics_' + tile.name.lower() + '_h */\n')

    with open(Path(__file__).parent / '../generated' / f'fd_metrics_{tile.name.lower()}.c', 'w') as f:
        f.write('/* THIS FILE IS GENERATED BY gen_metrics.py. DO NOT HAND EDIT. */\n')
        f.write(f'#include "fd_metrics_{tile.name.lower()}.h"\n\n')

        f.write(f'const fd_metrics_meta_t FD_METRICS_{tile.name}[FD_METRICS_{tile.name}_TOTAL] = {{\n')
        for metric in metrics:
            full_name = f'{tile.name}_{camel2snake(metric.name)}'
            _write_metric_descriptor(f, full_name, metric)
        f.write('};\n')

def _write_enums(enums: List[MetricEnum]):
    with open(Path(__file__).parent / '../generated' / 'fd_metrics_enums.h', 'w') as f:
        f.write('#ifndef HEADER_fd_src_disco_metrics_generated_fd_metrics_enums_h\n')
        f.write('#define HEADER_fd_src_disco_metrics_generated_fd_metrics_enums_h\n\n')
        f.write('/* THIS FILE IS GENERATED BY gen_metrics.py. DO NOT HAND EDIT. */\n\n')
        for enum in enums:
            f.write(f'#define FD_METRICS_ENUM_{camel2snake(enum.name)}_NAME "{camel2snake(enum.name).lower()}"\n')
            f.write(f'#define FD_METRICS_ENUM_{camel2snake(enum.name)}_CNT ({len(enum.values)}UL)\n')
            for idx, enum_value in enumerate(enum.values):
                f.write(f'#define FD_METRICS_ENUM_{camel2snake(enum.name)}_V_{camel2snake(enum_value.name)}_IDX  {idx}\n')
                f.write(f'#define FD_METRICS_ENUM_{camel2snake(enum.name)}_V_{camel2snake(enum_value.name)}_NAME "{camel2snake(enum_value.name).lower()}"\n')
            f.write('\n')
        f.write('#endif /* HEADER_fd_src_disco_metrics_generated_fd_metrics_enums_h */\n')

def write_codegen(metrics: Metrics):
    os.makedirs(Path(__file__).parent / '../generated', exist_ok=True)

    _write_common(metrics)
    for (tile, tile_metrics) in metrics.tiles.items():
        _write_tile(tile, tile_metrics)
    _write_enums(metrics.enums.values())

    print(f'Generated {metrics.count()} metrics for {len(metrics.tiles)} tiles')
